package org.anachronos.jeep.agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarFile;

public class InstrumentationProxyFactory {
	
	public final static class InstrumentationInvocationHandler implements InvocationHandler, InstrumentationJdk6 {
		private final Object instrumentation;

		private final Map<Method, Method> methodMapping = 
			new HashMap<Method, Method>();
		private final Map<Method, Object> methodReceiverMapping = 
			new HashMap<Method, Object>();
		
		public InstrumentationInvocationHandler(final Object instrumentation) {
			this.instrumentation = instrumentation;
			initMethodMapping();
		}
		
		private void initMethodMapping() {
			initMethodMapping(InstrumentationJdk5.class);
			initMethodMapping(InstrumentationJdk6.class);
		}

		private void initMethodMapping(final Class<?> clazz) {
			final Class<? extends Object> proxiedClazz = instrumentation.getClass();
			final Method[] declaredMethods = clazz.getDeclaredMethods();
			for (final Method declaredMethod : declaredMethods) {
				final String methodName = declaredMethod.getName();
				final Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
				try {
					final Method proxiedMethod = proxiedClazz.getDeclaredMethod(methodName, parameterTypes);
					methodMapping.put(declaredMethod, proxiedMethod);
					methodReceiverMapping.put(declaredMethod, instrumentation);
				} catch (SecurityException e) {
					// should not happen, ignore it
				} catch (NoSuchMethodException e) {
					try {
						final Method method = getClass().getDeclaredMethod(methodName, parameterTypes);
						methodMapping.put(declaredMethod, method);
						methodReceiverMapping.put(declaredMethod, this);
					} catch (Exception e1) {
						// should never happen!
						throw new IllegalStateException(e1);
					}
				} 
			}
		}

		@Override
		public Object invoke(final Object _, final Method method, final Object[] args)
				throws Throwable {
			final Method proxyMethod = methodMapping.get(method);
			Object proxy = methodReceiverMapping.get(method);
			Object result = null;
			try {
				result = proxyMethod.invoke(proxy, args);
			} catch(InvocationTargetException i) {
				throw i.getTargetException();
			}
			catch (Exception e) {
				throw e;
			}
			return result;
		}

		@Override
		public void addTransformer(ClassFileTransformer transformer,
				boolean canRetransform) {
			unsupportedOperation();
		}

		@Override
		public void appendToBootstrapClassLoaderSearch(JarFile jarFile) {
			unsupportedOperation();
		}

		@Override
		public void appendToSystemClassLoaderSearch(JarFile jarFile) {
			unsupportedOperation();
		}

		@Override
		public boolean isModifiableClass(Class<?> clazz) {
			return unsupportedOperation();
		}

		@Override
		public boolean isNativeMethodPrefixSupported() {
			return unsupportedOperation();
		}

		@Override
		public boolean isRetransformClassesSupported() {
			return false;
		}

		@Override
		public void retransformClasses(Class<?>... classes)
				throws UnmodifiableClassException {
			unsupportedOperation();
		}

		@Override
		public void setNativeMethodPrefix(ClassFileTransformer transformer,
				String prefix) {
			unsupportedOperation();
		}
		
		private boolean unsupportedOperation() {
			throw new UnsupportedOperationException();
		}
	};
	
	private InstrumentationProxyFactory() {
		// prevent instantiation
	}
	
	public static CommonInstrumentation newProxy(final Object instrumentation) {
		final Class<?>[] interfaces = new Class<?>[] { CommonInstrumentation.class };
		final InstrumentationInvocationHandler invocationHandler = 
			new InstrumentationInvocationHandler(instrumentation);
		final CommonInstrumentation commonInstrumentation = (CommonInstrumentation) Proxy.newProxyInstance(
				InstrumentationProxyFactory.class.getClassLoader(), interfaces, invocationHandler);
		return commonInstrumentation;
	}
}
